#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <map>

#include "messages.h"

typedef uint16_t PORT; /* Type of port */

struct message
{
	char text[1<<16]; /* message itself */
	int size;         /* number of bytes */
};

struct server
{
	std::string name;
	PORT com_port;
};

typedef int FILE_DESC; /* File descriptor type */
typedef std::map<PORT, server>::iterator s_iterator;

/**************************************************************
 *        CONSTANTS AND GLOBAL VARIABLES                      *
 *************************************************************/
/*communication port*/
const int CLIENT_PORT = 4000;

/* Server Map port to name */
std::map<PORT,server> servers;

/**************************************************************
 *                    PROTOTYPES                              *
 *************************************************************/

/* business functions */
void deal(message *pack);
void create_room(message *pack, int *number_of_rooms);
void create_new_room(std::string name, int *number_of_rooms);
void give_room_list(message *pack);
void delete_room(message *pack, int *number_of_rooms);

/* Communications functions */
void setUp(FILE_DESC *sock, sockaddr_in *bind_addr, int port, std::string who);
int communicate_server(message pack, int port); 
int receive(FILE_DESC sock, message *pack);

/* Auxiliary functions */
void error(const char *s);
int getLength(char *size);
int strconcat(char *destiny, char *origin, uint16_t start);
void debug(const char* msg);

/**************************************************************
 *                   IMPLEMENTATION                           *
 *************************************************************/
int main(int argc, char *argv[])
{
	sockaddr_in addr, c_bind_addr;
	FILE_DESC connected, c_sock;
	message pack;
	int addrlen = sizeof(addr), length;
	
	// setting up communications channels
	setUp(&c_sock, &c_bind_addr, CLIENT_PORT, "client");

	// waits forever for a client connection (state = CLOSED)
	while (true)
	{
		connected = accept(c_sock, (sockaddr*)&addr, (socklen_t*)&addrlen);		
		debug("Client conected");
		length = receive(connected, &pack);
		if(length != 0)
		{
			debug("Message received from client");
	
			deal(&pack);
			write(connected, pack.text, pack.size);
			
			debug("Message sent to client");
		}	
		else	debug("Communication failure");
		close(connected);
	}
}	

/**************************************************************
 *                   Business Functions                       *
 *************************************************************/

/* Call the right function to deal with the received message */
void deal(message *pack)
{
	int type = pack->text[2]; /* client message received */
	static int number_of_rooms = 0;
	switch(type)
	{
		case LC_ROOM_LIST: /* send to client CL_ROOM_LIST */
			give_room_list(pack);
			break;
		case LC_CREATE_ROOM: /* send to server CS_ROOM_CREATED after create a server */
			create_room(pack, &number_of_rooms);
			break;
		case LC_DELETE_ROOM: /* send to server CS_DELETE_ROOM */
			delete_room(pack, &number_of_rooms);
			break;
	}
}

/* 
	Put in pack the message CL_ROOM_LIST doing bytes:
		-> 0 and 1: message size
		-> 2: message type (0)
		-> 3: number of open rooms
		-> rest: a sequence of name (null-terminated string) and port (2 bytes int) of each server
*/
void give_room_list(message *pack)
{
	/* stay in CLOSED state */
	uint16_t open_rooms = 0; /* counter to the number of opened rooms*/
	uint16_t size = 4; /* starts with 2 bytes for size, 1 byte for type and 1 for number of rooms*/ 
	uint16_t aux;
	
	for(s_iterator it = servers.begin(); it != servers.end(); it++, open_rooms++)
	{
		size += strconcat((char*) pack->text, (char*) it->second.name.c_str(), size);
		aux = htons(it->first);
		memcpy(pack->text + size, &aux, sizeof(PORT));
		size += 2;
	}
	
	pack->size = size; 
	size = htons(size - 2); /* size bytes does not count */
	memcpy(pack->text, &size, sizeof(uint16_t)); /* setting pack->text[0] and pack->text[1] */
	pack->text[2] = CL_ROOM_LIST; /* message type */
	pack->text[3] = open_rooms;
}

/* 
   Treat the message from client to create room. 
   Delegate to create_new_room the real task of creating the room and communicate with the server.
*/
void create_room(message *pack, int *number_of_rooms)
{
	/* state CREATE_ROOM */
	std::string new_name;
	for(int i = 3; i < pack->size; i++)
		new_name += pack->text[i];
	
	debug(new_name.c_str());
	
	create_new_room(new_name, number_of_rooms);
	
	/* CL_ROOM_CREATED */
	pack->text[0] = 0;	pack->text[1] = 1; /* message length */
	pack->text[2] = CL_ROOM_CREATED; /* message type */
	pack->size = 3;
}

/* Receives a name from create_room and create a new room and send CS_PING to confirm that server is on */
void create_new_room(std::string name, int *number_of_rooms)
{
	static PORT port_client = 4002, port_coord = 3999;
	
	if(*number_of_rooms >= 127)	return;
	
	(*number_of_rooms)++;
	server serv;
	serv.name = name;
	serv.com_port = port_coord;
	servers[port_client] = serv;
	
	char arg[30];
	sprintf(arg, "%d %d", port_client++, port_coord--);
	
	int pid = fork();
	if(pid == 0)	/*filho*/
	{
		debug("Creating server...");
		system(((std::string("./server ") + std::string(arg))).c_str());
		exit(0);
	}
	else /* waiting son starts */
		sleep(1);
	
	message pack; /*CS_PING*/
	pack.text[2] = CS_PING; /*type*/
	pack.text[0] = 0;	pack.text[1] = 1; /*1 byte of content*/
	pack.size = 3;
		
	communicate_server(pack, serv.com_port);
}

/* Check if the given room can be deleted and do it. Success or failure is communicated in response message */
void delete_room(message *pack, int *number_of_rooms)
{
	/* state DELETE_ROOM */
	PORT port;
	memcpy(&port, pack->text + 3, sizeof(PORT));
	port = ntohs(port);
	
	if(servers.find(port) == servers.end())	return;
	
	pack->text[2] = CS_DELETE_ROOM; /* CS_DELETE_ROOM type */
	pack->text[0] = 0;	pack->text[1] = 1; /*1 byte after size */
	pack->size = 3;
	
	int type = communicate_server(*pack, servers[port].com_port);
	
	pack->text[0] = 0;	pack->text[1] = 1; /*1 byte after size */
	pack->size = 3;
	
	if(type == SC_ROOM_HAS_ASSOCIATES)	/* room has associates */
	{
		pack->text[2] = CL_ROOM_HAS_ASSOCIATES;
		debug("Room has associates");
	}
	else	if (type == SC_ROOM_DELETED) /* room deleted */
	{
		pack->text[2] = CL_ROOM_DELETED;
		servers.erase(port);
		(*number_of_rooms--);
		debug("Room deleted");
	}
}
	
/**************************************************************
 *                Communication Functions                     *
 *************************************************************/

/* Create the Sockt and bind the port. Erro message can be returned */
void setUp(FILE_DESC *sock, sockaddr_in *bind_addr, int port, std::string who)
{
	// opening socket
	*sock = socket(AF_INET, SOCK_STREAM, 0);
	if (*sock < 0) 
	{
		std::string s = "error opening" + who + "socket";
		error(s.c_str());
	}
	
	// binding socket
	memset(bind_addr, 0, sizeof(*bind_addr));
	bind_addr->sin_family = AF_INET;
	bind_addr->sin_addr.s_addr = INADDR_ANY;
	bind_addr->sin_port = htons(port);
	if (bind(*sock, (sockaddr*)bind_addr, sizeof(*bind_addr)) < 0)
		error(("error binding " + who).c_str());
	// listening to connections
	listen(*sock, 128);
}

/* Send message pack to server serv in the specified port */
int communicate_server(message pack, int port)
{		
	debug("Conecting to server");

	FILE_DESC s_sock;
	sockaddr_in s_bind_addr;
	s_sock = socket(AF_INET, SOCK_STREAM, 0);
	s_bind_addr.sin_port = htons(port);
	s_bind_addr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &s_bind_addr.sin_addr);
	if (connect(s_sock, (sockaddr*)&s_bind_addr, sizeof(s_bind_addr)) < 0)
	{
		error("error connecting to server");
		return -1;
	}
	
	debug("Sending message to server...");
	int send = 0;
	while(send < pack.size)
		send += write(s_sock, pack.text, pack.size);
	debug("Message sent to server");

	/* receive */
	receive(s_sock, &pack);
	close(s_sock);
	
	debug("Message received from server");
	return pack.text[2];
}

/**************************************************************
 *                Auxiliary Functions                         *
 *************************************************************/

/* Return the length of a received message (2 first bytes) */
int getLength(char *size)
{
	uint16_t length;
	memcpy(&length, size, sizeof(uint16_t));
	return ntohs(length);
}

void error(const char *s)
{
	perror(s);
	exit(1);
}

void debug(const char* msg)
{
	#ifdef DEBUG
	printf("%s\n",msg);
	fflush(stdout);
	#endif /*DEBUG*/
}

/*	
	Concatenate char[] destiny and char[] origin, starting at destiny[start].
	The return value consider the '\0' just as another character, so it will not be removed 
*/
int strconcat(char *destiny, char *origin, uint16_t start) 
{
	for(uint16_t i = start; i - start <= (uint16_t) strlen(origin); i++)
		destiny[i] = origin[i - start];
	return strlen(origin) + 1;
}

int receive(FILE_DESC sock, message *pack)
{
	int length = 0, read_now;
	while(length < 2)	
	{
		read_now = read(sock, pack->text + length, 2 - length);
		if(read_now < 0)	return 0;
		length += read_now;
	}
	pack->size = getLength(pack->text) + 2;
	while(length < pack->size)	
	{
		read_now = read(sock, pack->text + length, pack->size - length);
		if(read_now < 0)	return 0;
		length += read_now;
	}
	return length;
}
